module.exports = {


  friendlyName: 'Get vulnerabilities',


  description: 'Get sorted & paginated vulnerabilities that match the provided filters.',


  inputs: {

    minSeverity: {
      description: 'Optional filter to only get vulnerabilities whose `severity` is >= the specified value.',
      type: 'number',
      defaultsTo: 0,
    },

    maxSeverity: {
      description: 'Optional filter to only get vulnerabilities whose `severity` is <= the specified value.',
      type: 'number',
      defaultsTo: 10,
    },

    sortBy: {
      description: 'An optional facet to sort vulnerabilities by.',
      type: 'string',
      isIn: [
        'cveId',
        'severity',
        'hasKnownExploit',
        'publishedAt',
        'resolvedAt',
        'createdAt'
      ],
      defaultsTo: 'publishedAt'
    },

    sortDirection: {
      type: 'string',
      isIn: [
        'ASC',
        'DESC',
      ],
      defaultsTo: 'DESC'
    },

    page: {
      description: 'The zero-indexed page number.',
      type: 'number',
      defaultsTo: 0
    },

    pageSize: {
      description: 'The number of vulnerabilities to return per page',
      type: 'number',
      defaultsTo: 40,
    },

    teamApid: {
      description: 'The ID of the Team to filter by, or 0 to only include hosts with no team, or undefined to not filter by any team.',
      type: 'number',
    },

    cveId: {
      description: 'If provided, a report will be generated for only this CVE',
      type: 'string',
    },

    includeResolvedInstalls: {
      type: 'boolean',
      description: 'Whether or not to include resolved install information in this report.',
      extendedDescription: 'This is only used when generating a report for the CSV export actions.'
    },

  },

  exits: {

    success: {
      outputFriendlyName: 'Report',
      outputDescription: 'A dictionary with a `total` and `entries` (a list of vulnerabilities).',
      outputType: {},
      outputExample: {
        total: 1,
        entries: [
          { id: 99, /*…*/ }
        ]
      }
    },

    noMatchingVulnerabilities: {
      description: 'No vulnerabilities matching the provided filters were found.',
    },

  },


  fn: async function ({minSeverity, maxSeverity, sortBy, sortDirection, page, pageSize, teamApid, cveId, includeResolvedInstalls}) {
    let report = {};

    // If a specific CVE ID was provided, we'll change the where clause to only return information about that CVE.
    let whereClause;
    if(cveId){
      whereClause = { cveId };
    } else {
      whereClause = {
        and: [
          { severity: { '>=': minSeverity } },
          { severity: { '<=': maxSeverity } },
        ]
      };
    }//∞

    // Because we can't sort by resolvedAt until we've added that value, we'll sort by publishDate for now
    // and set a flag that we'll use to sort the list by resolvedAt values when we have that information added.
    let sortResultsyByResolveDate = false;
    if(sortBy === 'resolvedAt'){
      sortResultsyByResolveDate = true;
      sortBy = 'publishedAt';
    }

    // This bit of code is performance-sensitive.
    // It gathers three pieces of information:
    //   - report.total (total # distinct cves)
    //   - hostConstraint (constraing with {in: list of matching host ids})
    //   - vulnerabilities (list of flat vuln records)
    let vulnerabilities;
    let hostConstraint = { nin: [] };
    if (teamApid === undefined || cveId) {
      report.total = await Vulnerability.count(whereClause);
      vulnerabilities = await Vulnerability.find(whereClause)
      .paginate(page, pageSize)
      .sort([
        { [sortBy]: sortDirection }
      ]);
      if(vulnerabilities.length === 0){
        throw 'noMatchingVulnerabilities';
      }
    } else {// If we're filtering by a specific team and not getting the results for a single vulnerability, we'll build a list of vulnerabilities that affect that team.

      let hostIdsToFind = _.pluck(await Host.find({teamApid: teamApid}).select(['id']), 'id');
      // TODO use parameterization
      let firstNativeQueryToSend;

      if(sails.config.datastores.default.adapter === 'sails-postgresql') {
        firstNativeQueryToSend = `
        SELECT * FROM  "vulnerability"
        WHERE "id" IN (
        SELECT "vulnerability" FROM "vulnerabilityinstall"
          WHERE "host" IN (${hostIdsToFind.join(',')})
        );`;
      } else if(sails.config.datastores.default.adapter === 'sails-mysql') {
        firstNativeQueryToSend = 'SELECT * FROM  `vulnerability` '+
        'WHERE `id` IN ('+
          'SELECT `vulnerability` FROM `vulnerabilityinstall` '+
          'WHERE `host` IN ('+
            hostIdsToFind.join(',')+
          ')'+
        ');';
      }


      let rawResultFromDatabase = await sails.sendNativeQuery(firstNativeQueryToSend);

      for(let row of rawResultFromDatabase.rows){
        row.createdAt = Number(row.createdAt);
      }

      let vulnerabilitiesAffectingThisTeam = _.uniq(rawResultFromDatabase.rows, 'cveId');
      if(vulnerabilitiesAffectingThisTeam.length === 0){
        throw 'noMatchingVulnerabilities';
      }

      // Sort the filtered array of vulnerabilities fot this team
      if(sortDirection === 'ASC') {
        vulnerabilities = _.sortBy(vulnerabilitiesAffectingThisTeam, sortBy);
      } else {
        vulnerabilities = _(_.sortBy(vulnerabilitiesAffectingThisTeam, sortBy)).reverse().value();
      }

      // Filter the array of vulnerabilities for this team using the provided minSeverity and maxSeverity.
      let filteredVulnerabilities = _.filter(vulnerabilities, (vuln)=>{
        return vuln.severity >= minSeverity && vuln.severity <= maxSeverity;
      });
      // If there are no vulnerabilities matching the selected filters send a noMatchingVulnerabilities response.
      if(filteredVulnerabilities.length === 0){
        throw 'noMatchingVulnerabilities';
      }


      vulnerabilities = filteredVulnerabilities;
      hostConstraint = { in: hostIdsToFind };
      report.total = vulnerabilities.length;

      let startIndex = page * pageSize;
      let endIndex = startIndex + pageSize;
      let paginatedVulnerabilities = vulnerabilities.slice(startIndex, endIndex);
      vulnerabilities = paginatedVulnerabilities;
    }

    // Build a where clause for the native query we will be sending.
    let vulnerabilityInstallWhereClause;

    // If we're including resolved install information in this report for a CSV export, we'll send a query without the uninstalledAt condition.
    if(includeResolvedInstalls){
      vulnerabilityInstallWhereClause = `WHERE vulnerability IN (${_.pluck(vulnerabilities,'id').join(',')})`;// Note this where clause will work with Postgres and MySQL datastores.
    } else { // Otherwise, we'll send queries to only get unresolved installs.

      if(sails.config.datastores.default.adapter === 'sails-postgresql') {
        // If this app is configured to use a Postgres datastore, we'll need to put double quotes around the uninstalledAt column name.
        vulnerabilityInstallWhereClause = `WHERE "uninstalledAt" = 0 AND vulnerability IN (${_.pluck(vulnerabilities,'id').join(',')})`;
      } else if(sails.config.datastores.default.adapter === 'sails-mysql') {
        vulnerabilityInstallWhereClause = `WHERE uninstalledAt = 0 AND vulnerability IN (${_.pluck(vulnerabilities,'id').join(',')})`;
      }
    }

    // Build the native query to get affected installs for this vulnerability.
    let vulnerabilityInstallNativeQuery = `
     SELECT * FROM vulnerabilityinstall
     ${vulnerabilityInstallWhereClause}`;

    let selectedInstallsFromEntireOrg = await sails.sendNativeQuery(vulnerabilityInstallNativeQuery);

    let hostsFromEntireOrg = await Host.find();

    for(let install of selectedInstallsFromEntireOrg.rows){
      install.host = _.find(hostsFromEntireOrg, {id: install.host});
      if(!install.host){
        throw new Error(`Error: when mapping host records to VulnerabilityInstall records, a VulnerabilityInstall (id: ${install.id}) was found that is missing a "host" value! This is the database record that is missing a host: ${install}`);
      }
    }//∞


    // Add numAffectedHosts, resolvedAt, affectedTeams, and affectedHosts to each vulnerability in the report.
    await sails.helpers.flow.simultaneouslyForEach(vulnerabilities, async (vulnerability)=>{
      let theseInstallsToReport = [];
      if(teamApid === undefined) {
        theseInstallsToReport = _.where(selectedInstallsFromEntireOrg.rows, {vulnerability: vulnerability.id});
      } else {
        theseInstallsToReport = _.where(selectedInstallsFromEntireOrg.rows, {vulnerability: vulnerability.id, host: { teamApid: teamApid } });
      }

      // Because the same vuln can apply to the same host multiple different times through different software versions
      // this list of installs could contain the same host more than once.  So when we grab the host IDs, we'll also deduplicate them to produce our count.

      vulnerability.numAffectedHosts = _.uniq(_.pluck(theseInstallsToReport, 'host')).length;

      // Get resolution timestamp for each vulnerability.
      // (Or 0 if unresolved vulnerabilities are still installed on the hosts.)
      if (theseInstallsToReport.length >= 1) {

        // Note: if we're including resolved installs in theseInstallsToReport, this value will be set to 0 for each vulnerabiity,
        // this is fine because we will tracking resolution for each vulnerable install, instead of the vulnerability.
        vulnerability.resolvedAt = 0;

        let affectedInstalls = [];
        for (let install of theseInstallsToReport){
          affectedInstalls.push({name: install.softwareName, version:install.versionName, fleetApid: install.fleetApid, url: sails.config.custom.fleetBaseUrl+'/software/'+install.fleetApid, affectedHost: install.host.id, installedAt: install.installedAt, uninstalledAt: install.uninstalledAt, resolvedInVersion: install.resolvedInVersion});
        }
        vulnerability.affectedInstalls = affectedInstalls;
        vulnerability.affectedSoftware = _.uniq(affectedInstalls, 'fleetApid');
        // Add Affected teams for vulnerabilities that have unresolved installs (or resolved installs
        vulnerability.affectedTeams = _.uniq(_.pluck(theseInstallsToReport, 'host.teamDisplayName'));
      } else {// Note: if we're including resolved installs in this report. (Note: this will only happen for CSV exports of all software items)
        // Return the timestamp of the last uninstallation of this vuln amongst these hosts.
        let recentResolvedInstalls = await VulnerabilityInstall.find({
          uninstalledAt: { '!=': 0 },
          host: hostConstraint,
          vulnerability: vulnerability.id
        })
        .populate('host')
        .sort('uninstalledAt DESC').limit(1);
        if (!recentResolvedInstalls[0]) {
          sails.log.warn(`Expected vulnerability ${vulnerability.cveId} (id: ${vulnerability.id}) with no unresolved installs to have a history of at least one resolved install.  Otherwise, why is it here?`);
          vulnerability.resolvedAt = 0;
        } else {
          vulnerability.resolvedAt = recentResolvedInstalls[0].uninstalledAt;
          // If we're filtering by a team, we'll add the teamDisplayName to the affectedTeams array. otherwise, we'll send a blank value for this vulnerability.
          // This is done to keep the results looking consistent when filtering by a team, while making sure we are still showing resolved vulnerabilities for this team to the user.
          if(teamApid !== undefined) {
            vulnerability.affectedTeams =  [recentResolvedInstalls[0].host.teamDisplayName];
          }
          // Note: If we're not filtering vulnerabilities by a team, we won't send a affectedTeams value for resolved vulnerabilities. This will cause the column to appear as 'N/A' in the vulnerability list.
        }
      }//∞

      // Add Affected hosts from live installs.
      vulnerability.affectedHosts = _.uniq(_.pluck(theseInstallsToReport, 'host'));
    });//∞

    // If we're sorting by resolve date, then we'll sort the vulnerabilities after we've added a resolvedAt value.
    if(sortResultsyByResolveDate){
      if(sortDirection === 'ASC') {
        vulnerabilities = _.sortBy(vulnerabilities, 'resolvedAt');
      } else {
        vulnerabilities = _(_.sortBy(vulnerabilities, 'resolvedAt')).reverse().value();
      }
    }

    report.entries = vulnerabilities;

    return report;
  }


};

